﻿$import( "SmitearWebGL.Core.Geometry.Rect" );
$import( "SmitearWebGL.Objects.Entity" );
$import( "SmitearWebGL.Objects.Terrain.TerrainSection" );
$import( "SmitearWebGL.Programs.LightPrePass.Terrain.MaterialProgram" );

SmitearWebGL.Objects.Terrain.TerrainSystem = SmitearWebGL.Objects.Entity.extend(
{
	/* * * * * * * * constructor * * * * * * * */

	init : function( name, heightMap, width, height, length, shift )
	{

		/* * * * * * * * public property * * * * * * * */

		/* * * * * * * * private property * * * * * * * */

		this._super( name );
		this._registClass( "SmitearWebGL.Objects.Terrain.TerrainSystem" );

		this._programActived = false;
		this._program = null;
		this._shaderCache = {};
		this._material = null;

		this._sectorShift = SmitearWebGL.Core.Math.clamp( shift, 1, 7 );
		this._sectorUnits = 1 << shift;
		this._sectorVerts = this._sectorUnits + 1;
		this._totalVerts = this._sectorVerts * this._sectorVerts;
		this._totalIndex = - 1;

		this._heightMap = heightMap;
		this._tableWidth = this._heightMap.getWidth();
		this._tableHeight = this._heightMap.getHeight();

		this._sectorCountX = this._tableWidth >> this._sectorShift;
		this._sectorCountY = this._tableHeight >> this._sectorShift;

		this._worldExtents = new SmitearWebGL.Core.Geometry.Rect( - width / 2, width / 2,
		- height / 2, height / 2,
		- length / 2, length / 2 );
		this._worldSize = this._worldExtents.size();

		this._sectorSize = $V2( this._worldSize.x / this._sectorCountX, this._worldSize.z / this._sectorCountY );

		this._mapScale = $V3( this._worldSize.x / this._tableWidtht,
		this._worldSize.y / 255.0,
		this._worldSize.z / this._tableHeigh );

		this._heightTable = null;
		this._normalTable = null;
		this._numSectors = 0;
		this._sectorArray = null;

		this._vertexBuffer = null;
		this._indexBuffer = null;

		this._buildHeightAndNormalTables();
		this._buildVertexBuffer();
		this._buildIndexBuffer( this._sectorVerts, this._sectorVerts, 1, 1, this._sectorVerts, 0 );
		this._allocateSectors();
	}
	,

	/* * * * * * * * public method * * * * * * * */

	getVertexBuffer : function()
	{
		return this._vertexBuffer;
	}
	,
	getIndexBuffer : function()
	{
		return this._indexBuffer;
	}
	,
	getNumSectors : function()
	{
		return this._numSectors;
	}
	,
	getSectorCountX : function()
	{
		return this._sectorCountX;
	}
	,
	getSectorCountY : function()
	{
		return this._sectorCountY;
	}
	,
	getSectorShift : function()
	{
		return this._sectorShift;
	}
	,
	getSectorUnits : function()
	{
		return this._sectorUnits;
	}
	,
	getTotalVerts : function()
	{
		return this._totalVerts;
	}
	,
	getSectorSize : function()
	{
		return this._sectorSize;
	}
	,
	getWorldSize : function()
	{
		return this._worldSize;
	}
	,
	getMapScale : function()
	{
		return this._mapScale;
	}
	,
	getWorldExtents : function()
	{
		return this._worldExtents;
	}
	,
	readWorldHeightXY : function( mapX, mapY )
	{
		if ( mapX >= this._tableWidth ) mapX = this._tableWidth - 1;
		if ( mapY >= this._tableHeight ) mapY = this._tableHeight - 1;

		return this._heightTable[( mapY * this._tableWidth ) + mapX];
	}
	,
	readWorldHeight : function( mapIndex )
	{
		$THROW( mapIndex < this._tableWidth * this._tableHeight, "invalid index" );

		return this._heightTable[mapIndex];
	}
	,
	tableIndex : function( mapX, mapY )
	{
		if ( mapX >= this._tableWidth ) mapX = this._tableWidth - 1;
		if ( mapY >= this._tableHeight ) mapY = this._tableHeight - 1;

		return ( mapY * this._tableWidth ) + mapX;
	}
	,
	readWorldNormalXY : function( mapX, mapY )
	{
		if ( mapX >= this._tableWidth ) mapX = this._tableWidth - 1;
		if ( mapY >= this._tableHeight ) mapY = this._tableHeight - 1;

		var index = ( ( mapY * this._tableWidth ) + mapX ) * 3;

		return [this._normalTable[index], this._normalTable[index + 1], this._normalTable[index + 2]];
	}
	,
	calcWorldHeight : function( worldX, worldY )
	{
		var fMapX = ( worldX - this._worldExtents.x0 ) / this._worldExtents.sizeX();
		var fMapY = ( worldY - this._worldExtents.y0 ) / this._worldExtents.sizeY();

		return this.calcMapHeight( fMapX, fMapY );
	}
	,
	calcWorldNormal : function( normal, worldX, worldY )
	{
		var fMapX = ( worldX - this._worldExtents.x0 ) / this._worldExtents.sizeX();
		var fMapY = ( worldY - this._worldExtents.y0 ) / this._worldExtents.sizeY();

		this.calcMapNormal( normal, fMapX, fMapY );
	}
	,
	release : function()
	{
		this._programActived = false;
		this._program = null;
		for ( var program in this._shaderCache )
		{
			this._shaderCache[program].release();
			delete this._shaderCache[program]
		}
		this._shaderCache =
		{
		}
		;
		this._material = null;

		if ( this._sectorArray )
		{
			for ( var i = 0; i < this._numSectors; i ++ )
			{
				this._sectorArray[i].release;
			}
		}

		this._sectorShift = 0;
		this._sectorUnits = 0;
		this._sectorVerts = 0;
		this._totalVerts = 0;
		this._totalIndex = - 1;
		this._sectorSize = $V2();
		this._worldSize = $V3();
		this._mapScale = $V3();
		this._sectorCountX;
		this._sectorCountY;
		this._worldExtents = null;

		this._tableWidth = 0;
		this._tableHeight = 0;
		this._heightTable = null;
		this._normalTable = null;
		this._numSectors = 0;
		this._sectorArray = null;
		this._heightMap = null;

		this._vertexBuffer = null;
		this._indexBuffer = null;

		this._super();
	}
	,
	calcMapHeight : function( mapX, mapY )
	{
		var fMapX = mapX * ( this._tableWidth - 1 );
		var fMapY = mapY * ( this._tableHeight - 1 );

		var iMapX0 = Math.floor( fMapX );
		var iMapY0 = Math.floor( fMapY );

		fMapX -= iMapX0;
		fMapY -= iMapY0;

		iMapX0 = SmitearWebGL.Core.Math.clamp( iMapX0, 0, this._tableWidth - 1 );
		iMapY0 = SmitearWebGL.Core.Math.clamp( iMapY0, 0, this._tableHeight - 1 );

		var iMapX1 = SmitearWebGL.Core.Math.clamp( iMapX0 + 1, 0, this._tableWidth - 1 );
		var iMapY1 = SmitearWebGL.Core.Math.clamp( iMapY0 + 1, 0, this._tableHeight - 1 );

		var h0 = this.readWorldHeightXY( iMapX0, iMapY0 );
		var h1 = this.readWorldHeightXY( iMapX1, iMapY0 );
		var h2 = this.readWorldHeightXY( iMapX0, iMapY1 );
		var h3 = this.readWorldHeightXY( iMapX1, iMapY1 );

		var avgLo = ( h1 * fMapX ) + ( h0 * ( 1.0 - fMapX ) );
		var avgHi = ( h3 * fMapX ) + ( h2 * ( 1.0 - fMapX ) );

		return ( avgHi * fMapY ) + ( avgLo * ( 1.0 - fMapY ) );
	}
	,
	calcMapNormal : function( normal, mapX, mapY )
	{
		var fMapX = mapX * ( this._tableWidth - 1 );
		var fMapY = mapY * ( this._tableHeight - 1 );

		var iMapX0 = RealToInt32_chop( fMapX );
		var iMapY0 = RealToInt32_chop( fMapY );

		fMapX -= iMapX0;
		fMapY -= iMapY0;

		iMapX0 = SmitearWebGL.Core.Math.clamp( iMapX0, 0, this._tableWidth - 1 );
		iMapY0 = SmitearWebGL.Core.Math.clamp( iMapY0, 0, this._tableHeight - 1 );

		var iMapX1 = SmitearWebGL.Core.Math.clamp( iMapX0 + 1, 0, this._tableWidth - 1 );
		var iMapY1 = SmitearWebGL.Core.Math.clamp( iMapY0 + 1, 0, this._tableHeight - 1 );

		var h0 = this.readWorldNormalXY( iMapX0, iMapY0 );
		var h1 = this.readWorldNormalXY( iMapX1, iMapY0 );
		var h2 = this.readWorldNormalXY( iMapX0, iMapY1 );
		var h3 = this.readWorldNormalXY( iMapX1, iMapY1 );

		var avgLoX = ( h1[0] * fMapX ) + ( h0[0] * ( 1.0 - fMapX ) );
		var avgLoY = ( h1[1] * fMapX ) + ( h0[1] * ( 1.0 - fMapX ) );
		var avgLoZ = ( h1[2] * fMapX ) + ( h0[2] * ( 1.0 - fMapX ) );

		var avgHiX = ( h3[0] * fMapX ) + ( h2[0] * ( 1.0 - fMapX ) );
		var avgHiY = ( h3[1] * fMapX ) + ( h2[1] * ( 1.0 - fMapX ) );
		var avgHiZ = ( h3[2] * fMapX ) + ( h2[2] * ( 1.0 - fMapX ) );

		normal.x = ( avgHiX * fMapY ) + ( avgLoX * ( 1.0 - fMapY ) );
		normal.y = ( avgHiY * fMapY ) + ( avgLoY * ( 1.0 - fMapY ) );
		normal.z = ( avgHiZ * fMapY ) + ( avgLoZ * ( 1.0 - fMapY ) );
		normal.normalize();
	}
	,
	computeErrorMetricOfGrid : function( xVerts,  yVerts, xStep, yStep, heightMapX, heightMapY )
	{
		var result = 0.0;
		var total_rows = yVerts - 1;
		var total_cells = xVerts - 1;

		var start_vert = ( heightMapY * this._tableWidth ) + heightMapX;
		var lineStep = yStep * this._tableWidth;

		var invXStep = 1.0 / xStep;
		var invYStep = 1.0 / yStep;

		for ( var j = 0; j < total_rows; ++ j )
		{
			var indexA = start_vert;
			var indexB = start_vert + lineStep;
			var cornerA = this.readWorldHeight( indexA );
			var cornerB = this.readWorldHeight( indexB );

			for ( var i = 0; i < total_cells; ++ i )
			{
				// compute 2 new corner vertices
				var indexC = indexA + xStep;
				var indexD = indexB + xStep;

				// grab 2 new corner height values
				var cornerC = this.readWorldHeight( indexC );
				var cornerD = this.readWorldHeight( indexD );

				// setup the step values for
				// both triangles of this cell
				var stepX0 = ( cornerD - cornerA ) * invXStep;
				var stepY0 = ( cornerB - cornerA ) * invYStep;
				var stepX1 = ( cornerB - cornerC ) * invXStep;
				var stepY1 = ( cornerD - cornerC ) * invYStep;

				// find the max error for all points
				// covered by the two triangles
				var subIndex = indexA;
				for ( var y = 0; y < yStep; ++ y )
				{
					for ( var x = 0; x < xStep; ++ x )
					{
						var trueHeight = this.readWorldHeight( subIndex );
						++ subIndex;

						var intepolatedHeight;

						if ( y < ( xStep - x ) )
						intepolatedHeight = cornerA + ( stepX0 * x ) + ( stepY0 * y );
						else
						intepolatedHeight = cornerC + ( stepX1 * x ) + ( stepY1 * y );

						var delta = Math.abs( trueHeight - intepolatedHeight );

						result = Math.max( result, delta );

					}
					subIndex = indexA + ( y * this._tableWidth );
				}

				// save the corners for the next cell
				indexA = indexC;
				indexB = indexD;
				cornerA = cornerC;
				cornerB = cornerD;
			}

			start_vert += lineStep;
		}

		return result;
	}
	,
	setMaterial : function( material )
	{
		this._material = material;

		for ( var i = 0; i < this._sectorArray.length;
		++ i )
		{
			this._sectorArray[i]._material = material;
		}
	}
	,
	getMaterial : function()
	{
		return this._material;
	}
	,

	/* * * * * * * * private method * * * * * * * */

	_allocateSectors : function()
	{
		this._sectorArray = new Array( this._sectorCountX * this._sectorCountY );

		for ( var y = 0; y < this._sectorCountY; ++ y )
		{
			for ( var x = 0; x < this._sectorCountX; ++ x )
			{
				var heightMapX = x << this._sectorShift;
				var heightMapY = y << this._sectorShift;

				var name = "__" + this.name + "_section_" + x + "_" + y + "__";
				var terrainSection = new SmitearWebGL.Objects.Terrain.TerrainSection( name,
																					this,
																					x, y,
																					heightMapX, heightMapY,
																					this._sectorVerts,
																					this._sectorVerts,
																					this._sectorSize,
																					this._worldExtents );

				this._sectorArray[this._numSectors ++ ] = terrainSection;
				this.addChild( terrainSection );
			}
		}
	}
	,
	_buildHeightAndNormalTables : function()
	{
		this._heightTable = null;
		this._normalTable = null;

		var len = this._tableWidth * this._tableHeight;
		this._heightTable = new Array( len );
		this._normalTable = new Array( len * 3 );

		for ( var i = 0; i < this._normalTable.length; i += 3 )
		{
			this._normalTable[i + 0] = 0.0;
			this._normalTable[i + 1] = 1.0;
			this._normalTable[i + 2] = 0.0;
		}

		this._heightMap.generateHeightTable( this._heightTable, this._mapScale.y, this._worldExtents.y0, SmitearWebGL.Core.Math.Color.Channel.RED );
		this._heightMap.generateNormalTable( this._normalTable, ( this._tableWidth * this._worldExtents.sizeY() ) / this._worldExtents.sizeX(), SmitearWebGL.Core.Math.Color.Channel.RED );
	}
	,
	_buildVertexBuffer : function()
	{
		var i = 0, x = 0, y = 0;
		var cellSize = [this._sectorSize.x / this._sectorUnits, this._sectorSize.y / this._sectorUnits];
		var vertY = - this._sectorSize.y / 2.0, vertX = - this._sectorSize.x / 2.0;

		this._vertexBuffer = g_vertexBufferManager.createResource( "__terrain_system_" + this.name + "_vb__" );
		this._vertexBuffer.createElementDescription( 2, 4 * 4, 0, SmitearWebGL.Resources.Buffer.DataType.FLOAT, SmitearWebGL.Resources.Buffer.AttribLocation.POSITION0 );
		this._vertexBuffer.createElementDescription( 2, 4 * 4, 2 * 4, SmitearWebGL.Resources.Buffer.DataType.FLOAT, SmitearWebGL.Resources.Buffer.AttribLocation.TEXCOORD );

		var pVerts = this._vertexBuffer.lock( this._totalVerts * 4 );

		for ( y = 0; y < this._sectorVerts; ++ y )
		{
			vertX = - this._sectorSize.x / 2.0;

			for ( x = 0; x < this._sectorVerts; ++ x )
			{
				pVerts[i ++ ] = vertX;
				pVerts[i ++ ] = vertY;
				pVerts[i ++ ] = x / this._sectorUnits;
				pVerts[i ++ ] = 1.0 - y / this._sectorUnits;

				vertX += cellSize[0];
			}

			vertY += cellSize[1];
		}

		this._vertexBuffer.unlock();
	}
	,
	_buildIndexBuffer : function( xVerts, yVerts, xStep, yStep, stride, flags )
	{
		this._indexBuffer = g_indexBufferManager.createResource( "__terrain_system_" + this.name + "_ib__" );
		this._indexBuffer.createSingleStripGrid( xVerts, yVerts, xStep, yStep, stride, flags );

		this._totalIndex = this._indexBuffer.getElementDescriptions()[0].count;
	}
	,
	_getProgram : function( renderStage )
	{
		var program = null;

		switch ( renderStage )
		{
			case SmitearWebGL.Renderer.Renderer.RenderStage.DEPTH :
			program = SmitearWebGL.Renderer.Renderer.globalPrograms.terrainDepth;
			break;
			case SmitearWebGL.Renderer.Renderer.RenderStage.NORMAL :
			program = SmitearWebGL.Renderer.Renderer.globalPrograms.terrainNormal;
			break;
			case SmitearWebGL.Renderer.Renderer.RenderStage.MATERIAL :
			program = this._shaderCache[this._material.name];
			if ( ! program )
			{
				program = g_programManager.createResource( SmitearWebGL.Programs.LightPrePass.Terrain.MaterialProgram );
				program.generateShader(
				{
					model0 : this, model1 : this._sectorArray[0]
				}
				);
				this._shaderCache[this._material.name] = program;
			}
			break;
		}

		return program;
	}
}
);